package com.wei.czz.common.utils;

import com.wei.czz.common.exception.CzzException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;

/**
 * Created by IntelliJ IDEA.
 *
 * @author wyw
 * date: 2023-09-04 10:18:40
 * className: MD5Utils MD5加密工具类
 * version: 1.0
 * description:
 */
public class MD5Utils {

    private static final Logger log = LoggerFactory.getLogger(MD5Utils.class);

    /**
     * MD5加密
     * @param bytes 明文字节数组
     * @return 密文
     */
    public static String encodeMD5(byte[] bytes) {
        return encodeMD5(bytes, false);
    }

    /**
     * MD5加密
     * @param bytes 明文字节数组
     * @return 密文
     */
    public static String encodeMD5(byte[] bytes, boolean toUpper) {
        try {
            // 获取加密对象
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            // 加密操作
            return ByteUtils.bytesToHex(messageDigest.digest(bytes), toUpper);
        } catch (Exception e) {
            log.error("MD5加密失败。length={} toUpper={} message={}", bytes.length, toUpper, e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * MD5加密
     * @param plainText 明文
     * @return 密文
     */
    public static String encodeMD5(String plainText) {
        if (StringUtils.isBlank(plainText)) {
            log.error("MD5加密参数为空");
            throw new CzzException();
        }
        return doEncodeMD5(plainText, "", false);
    }

    /**
     * MD5加密
     * @param plainText 明文
     * @param secretKey 密钥
     * @return 密文
     */
    public static String encodeMD5(String plainText, String secretKey) {
        if (StringUtils.isBlank(plainText) || Objects.isNull(secretKey)) {
            log.error("MD5加密参数为空");
            throw new CzzException();
        }
        return doEncodeMD5(plainText, secretKey, false);
    }

    /**
     * MD5加密
     * 使用明文的前二十个字符当作密钥
     * @param plainText 明文
     * @return 密文
     */
    public static String encodeMD5BySelfSecret(String plainText) {
        if (StringUtils.isBlank(plainText)) {
            log.error("MD5加密参数为空");
            throw new CzzException();
        }
        String secretKey = plainText.substring(0, Math.min(20, plainText.length()));
        return doEncodeMD5(plainText, secretKey, false);
    }

    /**
     * MD5加密
     * @param plainText 明文
     * @param toUpper   密文转大写
     * @return 密文
     */
    public static String encodeMD5(String plainText, boolean toUpper) {
        if (StringUtils.isBlank(plainText)) {
            log.error("MD5加密参数为空");
            throw new CzzException();
        }
        return doEncodeMD5(plainText, "", toUpper);
    }

    /**
     * MD5加密
     * @param plainText 明文
     * @param secretKey 密钥
     * @param toUpper   密文转大写
     * @return 密文
     */
    public static String encodeMD5(String plainText, String secretKey, boolean toUpper) {
        if (StringUtils.isBlank(plainText) || Objects.isNull(secretKey)) {
            log.error("MD5加密参数为空");
            throw new CzzException();
        }
        return doEncodeMD5(plainText, secretKey, toUpper);
    }

    /**
     * MD5加密
     * 使用明文的前二十个字符当作密钥
     * @param plainText 明文
     * @param toUpper   密文转大写
     * @return 密文
     */
    public static String encodeMD5BySelfSecret(String plainText, boolean toUpper) {
        if (StringUtils.isBlank(plainText)) {
            log.error("MD5加密参数为空");
            throw new CzzException();
        }
        String secretKey = plainText.substring(0, Math.min(20, plainText.length()));
        return doEncodeMD5(plainText, secretKey, toUpper);
    }

    /**
     * 执行MD5加密
     * 将明文插入密钥中间，在进行MD5加密
     * @param plainText 明文
     * @param secretKey 密钥
     * @return 密文
     */
    private static String doEncodeMD5(String plainText, String secretKey, boolean toUpper) {
        byte[] plainTextBytes = plainText.getBytes(StandardCharsets.UTF_8);
        byte[] secretKeyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
        byte[] bytes = new byte[plainTextBytes.length + secretKeyBytes.length];
        int middleIndex = secretKeyBytes.length / 2;
        int index = 0;
        if (middleIndex > 0) {
            // 数组复制
            System.arraycopy(secretKeyBytes, 0, bytes, index, middleIndex);
            // 下标变更
            index += middleIndex;
        }
        System.arraycopy(plainTextBytes, 0, bytes, index, plainTextBytes.length);
        if (secretKeyBytes.length > middleIndex) {
            index += plainTextBytes.length;
            System.arraycopy(secretKeyBytes, middleIndex, bytes, index, secretKeyBytes.length - middleIndex);
        }
        try {
            // 获取加密对象
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            // 加密操作
            return ByteUtils.bytesToHex(messageDigest.digest(bytes), toUpper);
        } catch (Exception e) {
            log.error("MD5加密失败。plainText={} secretKey={} message={}", plainText, secretKey, e.getMessage(), e);
            throw new CzzException();
        }
    }

    /**
     * 文件MD5加密
     * @param file 文件对象
     * @return 密文
     */
    public static String fileMD5(File file) {
        if (Objects.isNull(file)) {
            log.error("文件参数为空");
            throw new CzzException();
        }
        if (!file.exists()) {
            log.error("文件不存在");
            throw new CzzException();
        }
        // 获取加密对象
        MessageDigest messageDigest;
        try {
            messageDigest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            log.error("获取MD5加密对象失败。 message={}", e.getMessage(), e);
            throw new CzzException();
        }

        try (FileInputStream fis = new FileInputStream(file);
             FileChannel channel = fis.getChannel()) {

            // 动态计算缓冲区大小(8KB-1MB之间)
            ByteBuffer buffer = FileUtils.dinamicByteBuffer(file.length());
            while (channel.read(buffer) != -1) {
                // 切换为读模式
                buffer.flip();
                // 将缓冲区内容写入到消息摘要中
                messageDigest.update(buffer);
                // 清空缓冲区，准备下一次写入
                buffer.clear();
            }
            // 完成文件读取后，将缓冲区内容写入到消息摘要中
            byte[] result = messageDigest.digest();
            // 结果封装
            return ByteUtils.bytesToHex(result);

        } catch (FileNotFoundException e) {
            log.error("文件MD5计算，文件不存在: {}", e.getMessage(), e);
            throw new CzzException();
        } catch (IOException e) {
            log.error("文件MD5计算，文件读取异常: {}", e.getMessage(), e);
            throw new CzzException();
        }
    }

}
